﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;
using System.Collections.Specialized;
using System.Collections;

namespace cs746
{
    class TAData
    {
        private HashSet<Node> _objects;
        private HashSet<Edge> _edges;
        private Dictionary<string,string> _objectInnerNames;
       // private StringDictionary _edgesInnerNames;
        private Dictionary<string, string> _systemNamespaces;
        private int _maxObjNumber = 0;
        //private int maxEdgeNumber = 0;

        public TAData()
        {
            _objects = new HashSet<Node>();
            _edges = new HashSet<Edge>();
            _objectInnerNames = new Dictionary<string, string>();
           // _edgesInnerNames = new StringDictionary();

            _systemNamespaces = new Dictionary<string, string>();
        }

        public bool AddObject(string name, ObjectType type) 
        {
            //I don't know regex all that well, but there may be a way to combine this into one line
            string nSpace = "";
            if (type == ObjectType.Class || type == ObjectType.Interface || type == ObjectType.Struct || type == ObjectType.Delegate)
            {
                if (name.Contains("."))
                    nSpace = name.Substring(0, name.LastIndexOf('.'));
                else
                    nSpace = "";
            }
            else if (type != ObjectType.File && type != ObjectType.Directory && type != ObjectType.ReferenceDirectory)
            {
                string t = Regex.Match(name, "\\s[^<:]*").Value.Trim();
                if (t == "" || !t.Contains('.')) nSpace = "";
                else nSpace = t.Substring(0, t.LastIndexOf('.')).Trim();
            }
            string innerName;
            if (_objectInnerNames.ContainsKey(name)) 
            { 
                innerName = _objectInnerNames[name];
                if (type != ObjectType.MethodReference && _objects.Contains(new Node(name, ObjectType.MethodReference, nSpace, innerName))) 
                {
                    _objects.Remove(new Node(name, ObjectType.MethodReference, nSpace, innerName));
                }
            }
            else 
            {
                innerName = string.Format("O{0}", _maxObjNumber + 1);
                _maxObjNumber++;
                _objectInnerNames.Add(name, innerName); 
            }
            var obj = new Node(name, type, nSpace, innerName);

            if (_objects.Contains(obj)) return false;
            _objects.Add(obj);

            

            return true;
        }

        public bool AddEdge(string startpoint, string endpoint, EdgeType type) 
        {
            string startInnerName;
            string endInnerName;
            if (_objectInnerNames.ContainsKey(startpoint)) startInnerName = _objectInnerNames[startpoint];
            else 
            {
                startInnerName = string.Format("O{0}", _maxObjNumber + 1);
                _maxObjNumber++;
                _objectInnerNames.Add(startpoint, startInnerName);
            }
            if (_objectInnerNames.ContainsKey(endpoint)) endInnerName = _objectInnerNames[endpoint];
            else
            {
                endInnerName = string.Format("O{0}", _maxObjNumber + 1);
                _maxObjNumber++;
                _objectInnerNames.Add(endpoint, endInnerName);
            }
            //string edgeInnerName = string.Format("{0}{1}", startInnerName, endInnerName);
            //if (!_edgesInnerNames.ContainsKey(edgeInnerName)) 
            //{
            //    _edgesInnerNames.Add(edgeInnerName, string.Format("F{0}", _edgesInnerNames.Count + 1));
            //}
            var edge = new Edge(startInnerName, endInnerName, type/*, _edgesInnerNames[edgeInnerName]*/); 
            if (_edges.Contains(edge)) return false;
            _edges.Add(edge);
            return true;
        }

        public List<Node> Objects { get { return new List<Node>(_objects); } }
        public List<Edge> Edges { get { return new List<Edge>(_edges); } }

        public void AddTypeFileLinks(string sourceCodeDirectory) 
        {
            var allCsFiles = GetAllCsFiles(sourceCodeDirectory);
            var nameRegex = new Regex("(\\w+\\.)+(?<name>\\w+)", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
            var innerObjects = new List<Node>(_objects);
            for (int j = 0; j < innerObjects.Count; j++) 
            {
                var type = innerObjects[j];
                if (type.Name.StartsWith("<PrivateImplementationDetails>")) continue;
                Regex r;
                var typeName = nameRegex.Match(type.Name).Groups["name"].Value;
                if (typeName == "") // regex didn't find any namespace declaration in the name
                    typeName = type.Name;
                if (type.Type == ObjectType.Interface)
                {
                    r = new Regex(string.Format("interface\\s+\\b{0}\\b", typeName), RegexOptions.Compiled | RegexOptions.Singleline);
                }
                else if (type.Type == ObjectType.Class)
                {
                    r = new Regex(string.Format("class\\s+\\b{0}\\b", typeName), RegexOptions.Compiled | RegexOptions.Singleline);
                }
                else if (type.Type == ObjectType.Struct) 
                {
                    r = new Regex(string.Format("struct\\s+\\b{0}\\b", typeName), RegexOptions.Compiled | RegexOptions.Singleline);
                }
                else if (type.Type == ObjectType.Delegate)
                {
                    r = new Regex(string.Format("delegate\\s+[\\w<>.]+\\s+\\b{0}\\b", typeName), RegexOptions.Compiled | RegexOptions.Singleline);
                }
                else continue;

                var fullFilename = LookForType(allCsFiles, typeName, type.Name, r);
                if (fullFilename == "")
                {
                    Console.WriteLine("Not found - {0}",type.Name);
                    continue; 
                }
                var filename = Path.GetFileName(fullFilename);
                var path = Path.GetDirectoryName(fullFilename).Substring(sourceCodeDirectory.Length); // TODO ????
                var arr = path.Split('\\');

                for (int i = 0; i < arr.Length; i++)
                {
                    if (arr[i] == "") continue;
                    AddObject(concatenate(arr,0,i), ObjectType.Directory);
                    if (i > 0) AddEdge(concatenate(arr, 0, i - 1), concatenate(arr, 0, i), EdgeType.Contains); 
                }
                AddObject(path + "\\" + filename, ObjectType.File);
                if (arr.Length > 0 && arr[0] != "") AddEdge(concatenate(arr, 0, arr.Length - 1), path+"\\"+filename, EdgeType.Contains);
                AddEdge(path + "\\" + filename, type.Name, EdgeType.Contains);

            }
        }

        private static string concatenate(string[] arr, int start, int end)
        {
            return concatenate(arr, start, end, '\\', false);
        }

        private static string concatenate(string[] arr, int start, int end, char separator, bool trimStart)
        {
            var builder = new StringBuilder();
            for (int i = start; i <= end; i++)
            {
                builder.AppendFormat("{0}{1}", separator, arr[i]); 
            }
            if (trimStart) builder = builder.Remove(0, 1);
            return builder.ToString();
        }

        private string GetNamespace(string sourcecode, Regex r)
        {
            //StreamReader reader = new StreamReader(fullfilename);
            //string sourcecode = reader.ReadToEnd();
            //reader.Close(); reader.Dispose();

            int typeposition = r.Match(sourcecode).Index;
            int leftbound = 0;
            var namespaceRegex = new Regex("namespace\\s+\\b(?<namespace>[\\w.]+)\\b\\s+{", RegexOptions.Compiled | RegexOptions.Singleline);
            List<string> namespaces = new List<string>();
            foreach (Match match in namespaceRegex.Matches(sourcecode))
            {
                if (match.Index < leftbound) continue;
                int balance = 0;
                namespaces.Add(match.Groups["namespace"].Value); 
                for (int i = sourcecode.IndexOf('{', match.Index); i < typeposition + 5; i++) 
                {
                    if (sourcecode[i] == '{') balance++;
                    else if (sourcecode[i] == '}') 
                    { 
                        balance--;
                        if (balance == 0) 
                        {
                            leftbound = i;
                            namespaces.RemoveAt(namespaces.Count - 1);
                            break;
                        }
                    }
                }
            }

            StringBuilder builder = new StringBuilder();
            if (namespaces.Count > 0) builder.Append(namespaces[0]);
            for (int i = 1; i < namespaces.Count; i++) 
            {
                builder.AppendFormat(".{0}", namespaces[i]); 
            }


            return builder.ToString(); 
        }      

        /// <summary>
        /// Return the return the filename
        /// </summary>
        /// <param name="sourceCodeDirectory"></param>
        /// <param name="r"></param>
        /// <returns></returns>
        private string LookForType(StringDictionary files, string typeName, string fullTypename, Regex r)
        {
            //var res = new List<string>();
            foreach (DictionaryEntry file in files)
            {
                //StreamReader reader = new StreamReader(file);
                //string sourcecode = reader.ReadToEnd();
                //reader.Close(); reader.Dispose();
                if (r.IsMatch(file.Value.ToString()))
                {
                    var ns = GetNamespace(file.Value.ToString(), r);
                    var trimmedFullName = fullTypename.Contains('`') ? fullTypename.Remove(fullTypename.IndexOf('`')) : fullTypename;
                    if (ns == "" && typeName == fullTypename) return file.Key.ToString();
                    if (ns != "" && string.Format("{0}.{1}", ns, typeName) == trimmedFullName) return file.Key.ToString();
                }
            }
            return "";
        }

        private static StringDictionary GetAllCsFiles(string sourceCodeDirectory) 
        {          
            var res = new StringDictionary();
            foreach (var file in Directory.GetFiles(sourceCodeDirectory, "*.cs"))
            {
                var reader = new StreamReader(file);
                string sourcecode = reader.ReadToEnd();
                reader.Close(); reader.Dispose();
                res.Add((file), sourcecode); 
            }
            foreach (var directory in Directory.GetDirectories(sourceCodeDirectory))
            {                
                var r = GetAllCsFiles(directory);
                foreach (DictionaryEntry pair in r)
                {
                    res.Add(pair.Key.ToString(), pair.Value.ToString());
                }
            }
            return res;
        }

        public void SaveToFile(string stubFileName, string taFileName)
        {
            StreamWriter outFile = new StreamWriter(taFileName);
            StreamReader inFile = new StreamReader(stubFileName);

            outFile.Write(inFile.ReadToEnd());  // Copy stub
            inFile.Close();
            inFile.Dispose();

            outFile.WriteLine("FACT TUPLE :");
            foreach (var node in Objects)
            {
                outFile.WriteLine(String.Format("$INSTANCE {0} {1}", node.InnerName, node.Type));
            }
            foreach (var edge in Edges)
            {
                outFile.WriteLine(String.Format("{0} {1} {2}", (edge.Type == EdgeType.Contains) ? "contain" : edge.Type.ToString(), edge.StartPoint, edge.EndPoint));
            }

            outFile.WriteLine("FACT ATTRIBUTE :");
            foreach (var node in Objects)
            {
                outFile.WriteLine(String.Format("{0} {{ label = \"{1}\" }}", node.InnerName, node.Name));
            }
            foreach (var edge in Edges)
            {
                outFile.WriteLine(String.Format("({0} {1} {2}) {{ }}", (edge.Type == EdgeType.Contains) ? "contain" : edge.Type.ToString(), edge.StartPoint, edge.EndPoint));
            }
            outFile.Close();
            outFile.Dispose();
        }

        public void MapUnknownNamespaces()
        {
            //Contains all nodes that do not have a source file corresponding
            //In this case, need to map by namespace
            HashSet<Node> unknownFiles = NodeWithoutSourceCode();//new HashSet<Node>();
            HashSet<String> systemNamespaces = new HashSet<String>();

            //foreach (Node n in unknownFiles)
            //{
            //    string[] namespaces = n.Namespace.Split('.');

            //    string t = "";
            //    foreach (string s in namespaces)
            //    {
            //        if (t == "")
            //            t = s;
            //        else
            //            t = t + "." + s;

            //        if (!systemNamespaces.Contains(t))
            //        {
            //            systemNamespaces.Add(t);
            //            AddObject(t, ObjectType.ReferenceDirectory);
            //            if (t.Contains('.')) AddEdge(t.Substring(0, t.LastIndexOf('.')), t, EdgeType.Contains);
            //        }
            //    }
            //}
            List<Edge> edges = new List<Edge>(_edges);
            foreach (var node in unknownFiles)
            {
                if (node.Namespace == "") continue;
                bool flSkip = false;
                if (node.Type == ObjectType.MethodReference) 
                {
                    for (int j = 0; j < edges.Count; j++)
                    {
                        if (edges[j].Type == EdgeType.Contains && node.InnerName == edges[j].EndPoint) { flSkip = true; break; }
                    }
                }
                if (flSkip) continue;
                string[] namespaces = node.Namespace.Split('.');
                for (int i = 0; i < namespaces.Length; i++)
                {
                    var nSpace = concatenate(namespaces, 0, i, '.', true); 
                    AddObject(nSpace, ObjectType.ReferenceDirectory);
                    if (i > 0) AddEdge(concatenate(namespaces, 0, i - 1, '.',true), nSpace, EdgeType.Contains); 
                }
                AddEdge(node.Namespace, node.Name, EdgeType.Contains);

            }
        }

        //private void AddSystemNamespace(Node obj)
        //{
        //    string[] namespaces = obj.Namespace.Split('.');
        //    //string cName = "";
        //    //if (obj.Type == ObjectType.MethodReference)
        //    //{
        //    //    cName = Regex.Match(obj.Name, "[^:<]*").Value;
        //    //    int i = cName.LastIndexOf('.');
        //    //    cName = cName.Substring(i + 1);
        //    //}

        //    if (namespaces[0] == "System")
        //    {
        //        if (!_systemNamespaces.ContainsKey("System"))
        //        {
        //            _systemNamespaces.Add("System", "System");
        //            AddObject("System", ObjectType.ReferenceDirectory);
        //        }
        //        AddEdge("System", obj.Name, EdgeType.Contains);
        //    }
        //    if (namespaces[0] == "Microsoft")
        //    {
        //        if (!_systemNamespaces.ContainsKey("Microsoft"))
        //        {
        //            _systemNamespaces.Add("Microsoft", "Microsoft");
        //            AddObject("Microsoft", ObjectType.ReferenceDirectory);
        //        }
        //        AddEdge("Microsoft", obj.Name, EdgeType.Contains);
        //    }
        //    if (namespaces[0] == "Mono")
        //    {
        //        if (!_systemNamespaces.ContainsKey("Mono.Cecil"))
        //        {
        //            _systemNamespaces.Add("Mono.Cecil", "Mono.Cecil");
        //            AddObject("Mono.Cecil", ObjectType.ReferenceDirectory);
        //        }
        //        AddEdge("Mono.Cecil", obj.Name, EdgeType.Contains);
        //    }
        //    if (namespaces[0] == "WeifenLuo")
        //    {
        //        if (!_systemNamespaces.ContainsKey("WeifenLuo"))
        //        {
        //            _systemNamespaces.Add("WeifenLuo", "WeifenLuo");
        //            AddObject("WeifenLuo", ObjectType.ReferenceDirectory);
        //        }
        //        AddEdge("WeifenLuo", obj.Name, EdgeType.Contains);
        //    }

        //    //string t = "";
        //    //foreach (string s in namespaces)
        //    //{
        //    //    if (t == "")
        //    //        t = s;
        //    //    else
        //    //        t = t + "." + s;

        //    //    if (!_systemNamespaces.ContainsKey(t))
        //    //    {
        //    //        _systemNamespaces.Add(t, t);
        //    //        AddObject(t, ObjectType.Directory);
        //    //        AddEdge(t.Substring(0, t.LastIndexOf('.')), t, EdgeType.Contains);
        //    //    }
        //    //}
        //    //t = t + "." + cName;
        //    //if (!_systemNamespaces.ContainsKey(t))
        //    //{
        //    //    _systemNamespaces.Add(t, t);
        //    //    AddObject(t, ObjectType.Directory);
        //    //    AddEdge(t.Substring(0, t.LastIndexOf('.')), t, EdgeType.Contains);
        //    //}

        //    //AddEdge(t, obj.Name, EdgeType.Contains);
        //}

        public void MakeItBetter()
        {
            #region remove all impicit classes with their functions
            List<Node> toBeRemovedNodes = new List<Node>();
            List<Edge> toBeRemovedEdges = new List<Edge>();
            HashSet<string> toBeRemovedInnerNames = new HashSet<string>();
            foreach (var obj in _objects)
            {
                if (obj.Name.Contains("<>c__DisplayClass") || obj.Name.Contains("<>f__AnonymousType")) { toBeRemovedNodes.Add(obj); toBeRemovedInnerNames.Add(obj.InnerName); }
            }
          

            foreach (var edge in _edges)
            {
                if (toBeRemovedInnerNames.Contains(edge.StartPoint) || toBeRemovedInnerNames.Contains(edge.EndPoint)) toBeRemovedEdges.Add(edge);
            }
            for (int i = 0; i < toBeRemovedNodes.Count; i++)
            {
                _objectInnerNames.Remove(toBeRemovedNodes[i].Name);
                _objects.Remove(toBeRemovedNodes[i]);
            }
            for (int i = 0; i < toBeRemovedEdges.Count; i++)
            {
                //_edgesInnerNames.Remove(toBeRemovedEdges[i].); // todo how to remove them ???
                _edges.Remove(toBeRemovedEdges[i]);
            }
            #endregion

            #region resolving nesting classes and reference methods
            var innerObjects = new List<Node>(_objects);
            for (int i = 0; i < innerObjects.Count; i++)
            {
                var obj = innerObjects[i];
                if (!obj.Name.Contains('/')) continue;
                if (obj.Type == ObjectType.Directory || obj.Type == ObjectType.File) continue;
                if (obj.Type == ObjectType.Ctor || obj.Type == ObjectType.Function || obj.Type == ObjectType.Interface || 
                    obj.Type == ObjectType.MethodReference || obj.Type == ObjectType.StaticFunction || obj.Type == ObjectType.VirtualFunction) 
                {                    
                    if (!obj.Name.Substring(obj.Name.IndexOf(' ')).Contains('/')) continue; // WARNING I wrote only for case when we have 1 level of nesting
                    string fatherClassName = obj.Name;
                    fatherClassName = fatherClassName.Substring(fatherClassName.IndexOf(' ') + 1);
                    fatherClassName = fatherClassName.Remove(fatherClassName.IndexOf('/'));
                    
                    //fatherClassName = _objectInnerNames[fatherClassName];
                    if (!_objectInnerNames.ContainsKey(fatherClassName)) 
                    {
                      //  Console.WriteLine("\t{0}",obj.Name);
                        continue; 
                    }
                    AddEdge(fatherClassName, obj.Name, EdgeType.Contains);
                    //Console.WriteLine("{0}", obj.Name);
                }
                else throw new NotImplementedException();
            }

            for (int i = 0; i < innerObjects.Count; i++)
            {
                var obj = innerObjects[i];
                if (obj.Type == ObjectType.MethodReference)
                {
                    var name = obj.Name.Substring(obj.Name.IndexOf(' ') + 1);
                    name = name.Remove(name.IndexOf("::"));
                    if (name.Contains('<')) name = name.Remove(name.IndexOf('<'));
                    if (!_objectInnerNames.ContainsKey(name))
                    {
                        continue;
                    }
                    AddEdge(name, obj.Name, EdgeType.Contains);
                }
            }
            #endregion

            #region Private stuff

            for (int i = 0; i < innerObjects.Count; i++)
            {
                var obj = innerObjects[i];
                if (obj.Name.StartsWith("<PrivateImplementationDetails>")) 
                {
                    if (!_objectInnerNames.ContainsKey("PrivateImplementationDetails")) 
                    {
                        AddObject("PrivateImplementationDetails", ObjectType.ReferenceDirectory); 
                    }
                    AddEdge("PrivateImplementationDetails", obj.Name, EdgeType.Contains); 
                }
            }

            #endregion
        }

        private HashSet<Node> NodeWithoutSourceCode() // need some "root" directory
        {
            var result = new HashSet<Node>(); // copy full list


            Dictionary<string, Node> reverseMap = new Dictionary<string, Node>();            
            List<Node> innerObjects = new List<Node>(_objects);
            for (int i = 0; i < innerObjects.Count; i++)
            {
                reverseMap.Add(innerObjects[i].InnerName, innerObjects[i]);
                if (innerObjects[i].Type == ObjectType.Class || innerObjects[i].Type == ObjectType.Delegate || innerObjects[i].Type == ObjectType.Interface || innerObjects[i].Type == ObjectType.Struct || innerObjects[i].Type == ObjectType.MethodReference)
                {
                    result.Add(innerObjects[i]);
                }
            }

            var analyzed = new HashSet<Node>();
            //List<Node> withCode = new List<Node>();
            for (int i = 0; i < innerObjects.Count; i++)
            {
                var obj = innerObjects[i];
                if (obj.Type == ObjectType.Directory) 
                {
                   // withCode.AddRange(NodeWithSourceCode(obj, reverseMap));

                    var res = (NodeWithSourceCode(obj, reverseMap,analyzed));
                    for (int j = 0; j < res.Count; j++)
                    {
                        result.Remove(res[j]); // remove all that has sources
                    }
                }
            }

            return result;
        }

        private List<Node> NodeWithSourceCode(Node directory, Dictionary<string, Node> map, HashSet<Node> analyzed) 
        {
            List<Node> res = new List<Node>();
            foreach (var edge in _edges)
            {
                if (edge.StartPoint == directory.InnerName) 
                {
                    var endpoint = map[edge.EndPoint];
                    if (analyzed.Contains(endpoint)) continue;
                    if (endpoint.Type == ObjectType.Class || endpoint.Type == ObjectType.Delegate || endpoint.Type == ObjectType.Interface || endpoint.Type == ObjectType.Struct || endpoint.Type == ObjectType.MethodReference) 
                    {
                        res.Add(endpoint);
                        res.AddRange(NodeWithSourceCode(endpoint, map, analyzed));
                    }
                    else if (endpoint.Type == ObjectType.Directory || endpoint.Type == ObjectType.File) 
                    {
                        analyzed.Add(endpoint);
                        res.AddRange(NodeWithSourceCode(endpoint, map,analyzed));
                    }
                }
            }

            return res;
        } 
    }

    struct Node 
    {
        private readonly string _name;
        private readonly ObjectType _type;
        private readonly string _namespace;
        private readonly string _innerName;

        public string Name 
        { 
            get 
            { 
                if (_type == ObjectType.Directory || _type == ObjectType.File) return Path.GetFileName(_name);
                else return _name; 
            } 
        }
        public ObjectType Type { get { return _type; } }
        public string Namespace { get { return _namespace; } }
        public string InnerName { get { return _innerName; } }

        public Node(string name, ObjectType type, string nspace, string innerName)
        {
            _name = name; 
            _type = type;
            _namespace = nspace;
            _innerName = innerName;
        }

        public override string ToString()
        {
            return string.Format("{0} - {1}", _name, _type);
        }
    }

    struct Edge 
    {
        private readonly string _startPoint;
        private readonly string _endPoint;
        private readonly EdgeType _type;
       // private readonly string _innerName;

        public string StartPoint { get { return _startPoint; } }
        public string EndPoint { get { return _endPoint; } }
        public EdgeType Type { get { return _type; } }
        //public string InnerName { get { return _innerName; } }

        public Edge(string startpoint, string endpoint, EdgeType type/*, string innerName*/)
        {
            _startPoint = startpoint;
            _endPoint = endpoint;
            _type = type;
            //_innerName = innerName;
        }

        public override string ToString()
        {
            return string.Format("{0} - {1} - {2}", _startPoint, _type, _endPoint);
        }
    }

    enum ObjectType 
    {
        Interface,
        Class,
        Delegate,
        Struct,
        Function, // put here all other "kinds" of functions
        VirtualFunction,
        StaticFunction,
        Ctor,
        MethodReference,
        File,
        Directory,
        ReferenceDirectory
    }

    enum EdgeType
    {
        Call,
        VirtualCall,
        RecursiveCall,
        Contains,
        CtorCall
    }
}
